Skip to content

feat: "Connect with Tolgee" OAuth login for in-context editing - #39

Draft
bdshadow wants to merge 8 commits into
mainfrom
bdshadow/oauth-authorization-server
Draft

feat: "Connect with Tolgee" OAuth login for in-context editing#39
bdshadow wants to merge 8 commits into
mainfrom
bdshadow/oauth-authorization-server

Conversation

@bdshadow

@bdshadow bdshadow commented Aug 12, 2026

Copy link
Copy Markdown
Member

Part of the cross-repo OAuth 2.1 work (see tolgee/tolgee-platform#3849). Adds a browser-redirect OAuth login so a contributor can authorize the extension with their own access instead of pasting a Project API Key.

Changes

  • "Connect with Tolgee" OAuth login via chrome.identity.launchWebAuthFlow + PKCE.
  • Token store and proactive refresh in the service worker; the refresh token never leaves the worker.
  • Inject the OAuth access token into the page on connect (from the background, since the popup closes during the auth flow) and update it in place on refresh.
  • Popup restructured around Login vs API-key tabs; select and bind the declared project for in-context editing.
  • Pin the extension id via a manifest key so the redirect URI is stable.

Draft — depends on the platform and tolgee-js branches of the same name.

Summary by CodeRabbit

  • New Features

    • Added secure OAuth login with PKCE authentication, token refresh, logout, and persistent sessions.
    • Added Login/OAuth and API-key authentication options.
    • Added server, branch, and project selection controls, including “All projects.”
    • Added automatic credential updates across open tabs without unnecessary page reloads.
    • Added connection status, access guidance, and project validation feedback.
  • Bug Fixes

    • Improved credential synchronization and handling of expired or invalid sessions.
  • Tests

    • Added automated test execution and coverage for authentication, validation, project selection, and popup state management.

Adds a browser-redirect OAuth 2.1 login alongside the existing API-key path:

- oauth/: PKCE (S256) via Web Crypto, authorization-code flow through
  chrome.identity.launchWebAuthFlow, token exchange + refresh, and a per-backend
  token store in the service worker.
- background: OAUTH_LOGIN/OAUTH_GET_TOKEN/OAUTH_LOGOUT handlers plus a
  chrome.alarms routine that proactively refreshes rotating tokens and pushes the
  new access token into matching tabs without reloading.
- content: injects the access token as __tolgee_authToken into page
  sessionStorage (the refresh token never leaves the service worker) and updates
  it in place on refresh.
- popup: a "Connect with Tolgee" button; OAuth sessions persist only a marker +
  backend url and re-fetch a fresh token on open, so a short-lived token is never
  stored stale.
- manifest: adds the "identity" and "alarms" permissions.
Make OAuth the primary sign-in: the "Connect with Tolgee" button sits directly
under the API url, and the API-key input + Apply are tucked into a collapsible
"API key sign in" block (collapsed by default). Also surface OAuth login
failures via console.error instead of swallowing them.
OAuth access tokens carry no embedded project (unlike a PAK), so the popup now
resolves one: it hints the page's configured project on connect, reads the
consented project back from the token's tg.prj and injects it into the page as
__tolgee_projectId, and shows a manual project picker only when the token is
bound to all projects.
Gives the unpacked extension a stable, deterministic id so its chromiumapp.org
redirect uri can be registered on the backend for local and preview OAuth testing.
…nnect

launchWebAuthFlow steals focus and closes the popup, so the popup's
post-login SET_CREDENTIALS never ran and the page never received the token
(users had to inject it by hand). The service worker now pushes the full
credential set (apiUrl, authToken, projectId) to the originating tab as soon
as login resolves, independent of the popup's lifecycle. The content script
reloads only when a value actually changed, so a redundant push doesn't
reload the page twice.
The OAuth (Login) path now resolves the project the page declares against the
connected server and injects it, or shows a clear "you can't edit this project
here" error when it isn't accessible — instead of leaving the token unscoped
and failing in-context with project_not_selected.

Extract the reducer into a pure factory, cover it and the helpers with vitest,
and run the tests in CI.
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: dcffe857-e9d9-4dc9-8a1e-6b3931e7ee58

📥 Commits

Reviewing files that changed from the base of the PR and between 9000afa and b3d9f42.

📒 Files selected for processing (1)
  • src/popup/TolgeeDetector.tsx

Walkthrough

The extension adds PKCE OAuth login, token refresh, session persistence, credential propagation, project scoping, popup authentication flows, reducer state management, and Vitest coverage.

Changes

OAuth authentication flow

Layer / File(s) Summary
OAuth protocol and session storage
src/oauth/*, src/constants.ts
Adds PKCE authorization, token exchange, refresh handling, normalized session storage, and OAuth configuration constants.
Runtime authentication propagation
.github/..., manifest.json, src/background/background.ts, src/content/contentScript.ts
Adds OAuth message handling, refresh alarms, credential injection, rotated-token updates, and required extension permissions.
Popup OAuth and project configuration
src/popup/TolgeeDetector.tsx, src/popup/reducer.ts, src/popup/storage.ts, src/popup/tools.ts, src/popup/useDetectorForm.tsx
Adds Login and API-key tabs, OAuth validation, project resolution, server and branch controls, and shared apply and clear actions.
Validation and CI coverage
src/popup/*.test.ts, vitest.config.ts, package.json, .github/workflows/test.yml
Adds Vitest configuration, reducer and utility tests, npm test scripts, and CI test execution.

Estimated code review effort: 4 (Complex) | ~60 minutes

Mergeability Score: 🔵 Low · up to b3d9f

The OAuth login can fail without telling the user why, and an unvalidated server address may produce malformed or unsafe navigation. The PR is mergeable with explicit owner awareness and follow-up on these bounded issues.

Sequence Diagram(s)

sequenceDiagram
  participant Popup
  participant Background
  participant OAuthClient
  participant TokenEndpoint
  participant ContentScript
  Popup->>Background: Request OAuth login
  Background->>OAuthClient: Start PKCE login
  OAuthClient->>TokenEndpoint: Exchange authorization code
  TokenEndpoint-->>OAuthClient: Return OAuth tokens
  OAuthClient-->>Background: Return OAuth tokens
  Background->>ContentScript: Inject access token and project ID
  ContentScript-->>Popup: Apply updated credentials
Loading

Possibly related PRs

  • tolgee/chrome-plugin#38: Overlaps with popup branch state, credential storage, content-script handling, and OAuth support.

Poem

A rabbit logs in, quick as a hop,
PKCE tokens flow and never stop.
Projects align, branches appear,
Fresh tokens reach each tab far and near.
Tests thump softly: green is the tune. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: adding Tolgee OAuth login for in-context editing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bdshadow/oauth-authorization-server

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (7)
src/background/background.ts (1)

75-77: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Log the messaging failure instead of discarding it.

The catch swallows every error. If the content script is not present, login appears to succeed but no credentials reach the page, and nothing records why.

Keep the non-throwing behavior, and log the reason.

🛠️ Proposed change
   await browser.tabs
     .sendMessage(tabId, { type: 'SET_CREDENTIALS', data })
-    .catch(() => undefined);
+    .catch((e) =>
+      console.debug('[tolgee-oauth] credential injection skipped', tabId, e)
+    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/background/background.ts` around lines 75 - 77, Update the sendMessage
error handler in the background messaging flow to capture the caught error and
log its reason, while preserving the existing non-throwing behavior and
undefined fallback.
src/oauth/tokenStore.ts (1)

7-12: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Document the storage choice for refresh tokens.

browser.storage.local persists unencrypted on disk and survives browser restarts. It holds the refresh token here. That is a deliberate trade-off for long-lived sessions, but it differs from browser.storage.session, which stays in memory.

Record the reason in the comment, and confirm the platform enforces an absolute refresh-token lifetime so a stale on-disk token cannot be replayed indefinitely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/oauth/tokenStore.ts` around lines 7 - 12, Update the comment above
saveSession to document that browser.storage.local stores refresh tokens
unencrypted on disk across browser restarts as a deliberate long-lived-session
trade-off versus browser.storage.session, and state the platform’s enforced
absolute refresh-token lifetime that prevents indefinite replay of stale tokens.
src/constants.ts (1)

14-25: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider narrowing the requested scopes.

The access token is written into page-accessible storage (AUTH_TOKEN_LOCAL_STORAGE). Any script running on that page can read it. Backend intersection with user permissions prevents privilege escalation, but it does not limit what a hostile page script can do with the token inside the user's own rights. screenshots.delete and keys.edit are destructive.

Request only the scopes the in-context editor actually calls, or request scopes incrementally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/constants.ts` around lines 14 - 25, Update OAUTH_SCOPES to include only
the permissions directly required by the in-context editor, removing destructive
scopes such as screenshots.delete and keys.edit unless their corresponding
operations are actually invoked. If those operations are needed conditionally,
request their scopes incrementally rather than in the default token scope set.
src/oauth/oauthClient.ts (1)

31-39: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Do not put the raw response body in the error, and add a request timeout.

The thrown message embeds the full token-endpoint body. src/background/background.ts (Lines 45-46) logs that error and returns String(e) to the popup. A token endpoint can echo request parameters in an error body, so a refresh token or authorization code can reach the console and the popup.

fetch also has no AbortSignal. apiUrl is user-supplied, so a non-responsive host stalls the login and refresh paths.

🛠️ Proposed fix
   const res = await fetch(`${base}/oauth2/token`, {
     method: 'POST',
     headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
     body: new URLSearchParams(params),
+    signal: AbortSignal.timeout(15_000),
   });
   if (!res.ok) {
-    const body = await res.text().catch(() => '');
-    throw new Error(`Tolgee token endpoint returned ${res.status}: ${body}`);
+    const body = await res.json().catch(() => null);
+    const reason = body && typeof body.error === 'string' ? body.error : '';
+    throw new Error(
+      `Tolgee token endpoint returned ${res.status}${reason ? `: ${reason}` : ''}`
+    );
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/oauth/oauthClient.ts` around lines 31 - 39, Update the token request in
the OAuth client’s fetch flow to avoid including the raw response body in thrown
errors; report only the safe HTTP status or a generic failure message. Add an
AbortSignal-based timeout to the fetch request, using the project’s established
timeout convention if available, so user-supplied apiUrl hosts cannot stall
login or refresh indefinitely.
src/popup/storage.ts (1)

25-39: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the shared Values type instead of the local intersection.

src/popup/tools.ts already declares Values with authToken and projectId. This file keeps a second local Values and then widens it with & { authToken?: string }. The two definitions can drift, and a reader cannot tell which one is authoritative. Import the type from ./tools and delete the local copy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/popup/storage.ts` around lines 25 - 39, Update storeValues to import and
use the shared Values type from ./tools directly, removing the local Values
definition and the authToken intersection. Preserve the existing optional values
handling and storage behavior.
src/popup/useDetectorForm.tsx (1)

245-296: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Add the resolution inputs to the dependency array.

The effect reads libConfig?.config?.projectId, checkableValues.apiUrl, and checkableValues.authToken, but it only depends on state.credentialsCheck. If the page reports a new projectId through TOLGEE_CONFIG_LOADED while credentialsCheck stays the same object, the popup keeps the previously resolved project. Add the read values to the dependency list.

♻️ Proposed dependency change
-  }, [state.credentialsCheck]);
+  }, [
+    state.credentialsCheck,
+    (libConfig?.config as { projectId?: number | string } | undefined)
+      ?.projectId,
+    checkableValues?.apiUrl,
+    checkableValues?.authToken,
+  ]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/popup/useDetectorForm.tsx` around lines 245 - 296, Update the dependency
array of the project-resolution useEffect to include
libConfig?.config?.projectId and the read checkableValues.apiUrl and
checkableValues.authToken values alongside state.credentialsCheck, so resolution
reruns when any input changes.
src/popup/sendToBackground.ts (1)

4-6: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Handle sendMessage rejections in this helper.

browser.runtime.sendMessage rejects when the service worker does not answer, for example after a worker restart or when the message channel closes. The callers (handleConnect in src/popup/TolgeeDetector.tsx and onLibConfigChange in src/popup/useDetectorForm.tsx) do not catch the rejection, so the popup produces an unhandled rejection and the user sees no feedback. Return a normalized error result here, or add catch at every call site.

♻️ Proposed helper change
-export const sendToBackground = async (type: string, data?: any) => {
-  return browser.runtime.sendMessage({ type, data });
-};
+export const sendToBackground = async (type: string, data?: any) => {
+  try {
+    return await browser.runtime.sendMessage({ type, data });
+  } catch (e) {
+    console.error(e);
+    return { error: String(e) };
+  }
+};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/popup/sendToBackground.ts` around lines 4 - 6, Update sendToBackground to
catch rejections from browser.runtime.sendMessage and return a normalized error
result instead of allowing the promise rejection to propagate. Preserve the
existing message payload and successful response behavior, using the helper’s
return contract so callers such as handleConnect and onLibConfigChange receive a
consistent failure result.
🔇 Additional comments (24)
src/popup/reducer.test.ts (1)

1-207: LGTM!

src/popup/tools.test.ts (1)

1-123: LGTM!

vitest.config.ts (1)

1-10: LGTM!

package.json (1)

19-21: LGTM!

Also applies to: 51-52

.github/workflows/test.yml (1)

33-35: LGTM!

src/constants.ts (2)

4-11: LGTM!


12-13: 🩺 Stability & Availability | ⚡ Quick win

Verify the refresh skew against the backend access-token lifetime.

OAUTH_REFRESH_SKEW_MS is 60s, but the background refresh alarm in src/background/background.ts (Line 82) runs every 10 minutes. If the backend issues access tokens with a lifetime under about 11 minutes, a token can expire in the page before the next alarm fires. The page-side token is only rotated by that alarm.

Confirm the platform access-token TTL, then align the alarm period with it (for example, period ≤ TTL/2) or derive the period from expiresAt.

src/oauth/pkce.ts (1)

3-23: LGTM!

src/oauth/oauthClient.ts (1)

84-96: LGTM!

src/oauth/tokenStore.ts (1)

26-31: LGTM!

manifest.json (2)

5-5: 🩺 Stability & Availability

Confirm the pinned key matches the Web Store item.

The key field pins the extension ID so identity.getRedirectURL() stays stable, which the OAuth redirect URI depends on. The value is a public key, so publishing it is safe.

The key must match the one the Chrome Web Store assigned to this item. If it does not match, the store-installed build gets a different ID and the pre-registered redirect URI stops matching. Confirm the value against the store listing, and confirm Firefox builds do not need this field.


18-18: LGTM!

src/background/background.ts (2)

4-16: LGTM!


100-113: 🩺 Stability & Availability

⚠️ Unverified finding
Sandbox verification was unavailable.

Verify the content script rejects tokens for a non-matching apiUrl.

browser.tabs.query({}) returns every tab. This function then sends the OAuth access token to each one. The token reaches every tab that runs the content script, including tabs where the user never applied Tolgee credentials.

The design depends on the content script comparing data.apiUrl against the backend already applied in that tab, and discarding the message otherwise. src/content/contentScript.ts is not in this review context, so that guard is not confirmed here.

Confirm the guard exists and compares origins, not raw strings. Alternatively, track which tabs received credentials and send only to those.

src/content/contentScript.ts (2)

4-6: LGTM!

Also applies to: 18-37


84-110: LGTM!

src/popup/tools.ts (1)

5-16: LGTM!

Also applies to: 18-19, 25-32, 38-61

src/popup/storage.ts (1)

7-11: LGTM!

Also applies to: 66-67

src/popup/reducer.ts (2)

1-72: LGTM!


156-244: LGTM!

src/popup/useDetectorForm.tsx (1)

6-17: LGTM!

Also applies to: 75-90, 133-192

src/popup/TolgeeDetector.tsx (3)

1-37: LGTM!

Also applies to: 48-68


122-205: LGTM!

Also applies to: 207-243, 269-281


284-358: LGTM!

Also applies to: 375-421, 423-485

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/background/background.ts`:
- Around line 49-56: Update the OAUTH_GET_TOKEN and OAUTH_LOGOUT handlers to
attach rejection handlers to their promises, ensuring sendResponse is invoked
with an error response when getValidAccessToken or clearSession rejects,
including malformed apiUrl and storage failures. Preserve the existing success
responses and return true behavior, matching the error-handling pattern used by
OAUTH_LOGIN.
- Around line 80-97: Update the top-level alarm setup around REFRESH_ALARM to
first check whether the alarm already exists, creating it with the 10-minute
period only when absent. Keep the browser.alarms.onAlarm listener registration
at the top level and preserve its existing refresh handling.

In `@src/content/contentScript.ts`:
- Around line 112-117: Update the UPDATE_AUTH_TOKEN listener to validate that
data.authToken is present and non-empty before calling sessionStorage.setItem;
retain the same-origin check and skip the write when the token is missing so an
invalid "undefined" value cannot reach the SDK.

In `@src/oauth/oauthClient.ts`:
- Around line 16-24: Update parseTokenResponse to validate that
data.access_token is present and reject the response before constructing
OAuthTokens when it is missing. Replace the zero-second expires_in fallback with
a conservative default lifetime so getValidAccessToken does not treat tokens
with omitted expiry as immediately expired; preserve the existing refresh-token
fallback behavior.
- Around line 61-73: Update the authorization flow around authorizeUrl and
redirectResponse to retain the generated state, parse the redirect URL once, and
reject responses whose returned state differs or is missing. Before requiring
code, detect redirect error and error_description parameters and throw an error
that includes the provider’s failure reason; preserve the existing missing-code
validation for responses without either a code or provider error.

In `@src/oauth/tokenStore.ts`:
- Around line 35-57: Update getValidAccessToken to deduplicate concurrent
refreshes by caching one in-flight refresh promise per apiUrl, reusing it for
overlapping callers, and removing it when settled. Preserve existing
token/session behavior, but only clear the session when refresh reports an
authentication failure; propagate or retain the session for other errors instead
of treating every failure as invalid credentials.

In `@src/popup/reducer.ts`:
- Around line 131-155: Update the APPLY_VALUES case in the reducer so
appliedValues and storedValues preserve the existing OAuth authToken and
projectId alongside apiKey, apiUrl, and the conditionally effective branch.
Ensure the OAuth values remain intact when APPLY_VALUES is triggered through the
Server field without changing the existing branchEnabled behavior.

In `@src/popup/TolgeeDetector.tsx`:
- Around line 85-118: Update handleConnect to capture and store res.error when
OAuth login fails, including cases where no accessToken is returned, and
preserve clearing the error on a new attempt or successful login. Render the
stored error message in the Login tab near the connect control so the user sees
why authentication failed.
- Around line 359-374: Validate values?.apiUrl before passing it to the Link
href in the server connection UI: accept only http: and https: URLs, and use
DEFAULT_SERVER for any other value, including malformed or javascript: schemes.
Keep the existing serverHost display and link behavior unchanged for valid URLs.

---

Nitpick comments:
In `@src/background/background.ts`:
- Around line 75-77: Update the sendMessage error handler in the background
messaging flow to capture the caught error and log its reason, while preserving
the existing non-throwing behavior and undefined fallback.

In `@src/constants.ts`:
- Around line 14-25: Update OAUTH_SCOPES to include only the permissions
directly required by the in-context editor, removing destructive scopes such as
screenshots.delete and keys.edit unless their corresponding operations are
actually invoked. If those operations are needed conditionally, request their
scopes incrementally rather than in the default token scope set.

In `@src/oauth/oauthClient.ts`:
- Around line 31-39: Update the token request in the OAuth client’s fetch flow
to avoid including the raw response body in thrown errors; report only the safe
HTTP status or a generic failure message. Add an AbortSignal-based timeout to
the fetch request, using the project’s established timeout convention if
available, so user-supplied apiUrl hosts cannot stall login or refresh
indefinitely.

In `@src/oauth/tokenStore.ts`:
- Around line 7-12: Update the comment above saveSession to document that
browser.storage.local stores refresh tokens unencrypted on disk across browser
restarts as a deliberate long-lived-session trade-off versus
browser.storage.session, and state the platform’s enforced absolute
refresh-token lifetime that prevents indefinite replay of stale tokens.

In `@src/popup/sendToBackground.ts`:
- Around line 4-6: Update sendToBackground to catch rejections from
browser.runtime.sendMessage and return a normalized error result instead of
allowing the promise rejection to propagate. Preserve the existing message
payload and successful response behavior, using the helper’s return contract so
callers such as handleConnect and onLibConfigChange receive a consistent failure
result.

In `@src/popup/storage.ts`:
- Around line 25-39: Update storeValues to import and use the shared Values type
from ./tools directly, removing the local Values definition and the authToken
intersection. Preserve the existing optional values handling and storage
behavior.

In `@src/popup/useDetectorForm.tsx`:
- Around line 245-296: Update the dependency array of the project-resolution
useEffect to include libConfig?.config?.projectId and the read
checkableValues.apiUrl and checkableValues.authToken values alongside
state.credentialsCheck, so resolution reruns when any input changes.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d5e2981b-9bed-4f44-8203-5d49be78f564

📥 Commits

Reviewing files that changed from the base of the PR and between 5f6502d and 9000afa.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (18)
  • .github/workflows/test.yml
  • manifest.json
  • package.json
  • src/background/background.ts
  • src/constants.ts
  • src/content/contentScript.ts
  • src/oauth/oauthClient.ts
  • src/oauth/pkce.ts
  • src/oauth/tokenStore.ts
  • src/popup/TolgeeDetector.tsx
  • src/popup/reducer.test.ts
  • src/popup/reducer.ts
  • src/popup/sendToBackground.ts
  • src/popup/storage.ts
  • src/popup/tools.test.ts
  • src/popup/tools.ts
  • src/popup/useDetectorForm.tsx
  • vitest.config.ts

Comment on lines +49 to +56
case 'OAUTH_GET_TOKEN':
getValidAccessToken(data.apiUrl).then((accessToken) =>
sendResponse({ accessToken })
);
return true;
case 'OAUTH_LOGOUT':
clearSession(data.apiUrl).then(() => sendResponse({}));
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add rejection handlers to OAUTH_GET_TOKEN and OAUTH_LOGOUT.

Both cases return true to keep the message channel open, but neither attaches a catch. If the promise rejects, sendResponse never runs and the caller waits on a channel that closes without a reply.

This path is reachable. keyFor in src/oauth/tokenStore.ts (Line 8) calls new URL(apiUrl), which throws a TypeError when the popup passes a malformed apiUrl. browser.storage.local calls can also reject.

The OAUTH_LOGIN case at Line 44 already handles this correctly.

🛠️ Proposed fix
     case 'OAUTH_GET_TOKEN':
-      getValidAccessToken(data.apiUrl).then((accessToken) =>
-        sendResponse({ accessToken })
-      );
+      getValidAccessToken(data.apiUrl)
+        .then((accessToken) => sendResponse({ accessToken }))
+        .catch((e) => {
+          console.error('[tolgee-oauth] token lookup failed', e);
+          sendResponse({ accessToken: null, error: String(e) });
+        });
       return true;
     case 'OAUTH_LOGOUT':
-      clearSession(data.apiUrl).then(() => sendResponse({}));
+      clearSession(data.apiUrl)
+        .then(() => sendResponse({}))
+        .catch((e) => {
+          console.error('[tolgee-oauth] logout failed', e);
+          sendResponse({ error: String(e) });
+        });
       return true;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case 'OAUTH_GET_TOKEN':
getValidAccessToken(data.apiUrl).then((accessToken) =>
sendResponse({ accessToken })
);
return true;
case 'OAUTH_LOGOUT':
clearSession(data.apiUrl).then(() => sendResponse({}));
return true;
case 'OAUTH_GET_TOKEN':
getValidAccessToken(data.apiUrl)
.then((accessToken) => sendResponse({ accessToken }))
.catch((e) => {
console.error('[tolgee-oauth] token lookup failed', e);
sendResponse({ accessToken: null, error: String(e) });
});
return true;
case 'OAUTH_LOGOUT':
clearSession(data.apiUrl)
.then(() => sendResponse({}))
.catch((e) => {
console.error('[tolgee-oauth] logout failed', e);
sendResponse({ error: String(e) });
});
return true;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/background/background.ts` around lines 49 - 56, Update the
OAUTH_GET_TOKEN and OAUTH_LOGOUT handlers to attach rejection handlers to their
promises, ensuring sendResponse is invoked with an error response when
getValidAccessToken or clearSession rejects, including malformed apiUrl and
storage failures. Preserve the existing success responses and return true
behavior, matching the error-handling pattern used by OAUTH_LOGIN.

Comment on lines +80 to +97
// Keep stored sessions fresh so the popup and the injected page token don't expire mid-use. Rotation means each
// refresh mints a new access + refresh token; getValidAccessToken persists them and pushes the access token to tabs.
browser.alarms.create(REFRESH_ALARM, { periodInMinutes: 10 });
browser.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name !== REFRESH_ALARM) {
return;
}
const sessions = await loadAllSessions();
for (const session of sessions) {
if (session.expiresAt - OAUTH_REFRESH_SKEW_MS > Date.now()) {
continue;
}
const accessToken = await getValidAccessToken(session.apiUrl);
if (accessToken) {
await pushTokenToTabs(session.apiUrl, accessToken);
}
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Create the alarm only when it does not already exist.

Line 82 runs at the top level of the service worker and calls browser.alarms.create unconditionally. An MV3 service worker restarts after about 30 seconds of idle time, so this line re-runs on every wake. Creating an alarm with an existing name replaces it and restarts the 10-minute schedule.

If the extension receives events more often than every 10 minutes, the alarm never reaches its fire time, and stored sessions never refresh. The Chrome alarms documentation recommends checking for the alarm on each service worker start and creating it only when it is absent. The docs state it is best to make sure important alarms exist each time your service worker starts up, using chrome.alarms.get before create.

Keep the onAlarm listener registration at the top level. That part is correct.

🛠️ Proposed fix
-browser.alarms.create(REFRESH_ALARM, { periodInMinutes: 10 });
+const ensureRefreshAlarm = async () => {
+  const existing = await browser.alarms.get(REFRESH_ALARM);
+  if (!existing) {
+    await browser.alarms.create(REFRESH_ALARM, { periodInMinutes: 10 });
+  }
+};
+ensureRefreshAlarm();
+
 browser.alarms.onAlarm.addListener(async (alarm) => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Keep stored sessions fresh so the popup and the injected page token don't expire mid-use. Rotation means each
// refresh mints a new access + refresh token; getValidAccessToken persists them and pushes the access token to tabs.
browser.alarms.create(REFRESH_ALARM, { periodInMinutes: 10 });
browser.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name !== REFRESH_ALARM) {
return;
}
const sessions = await loadAllSessions();
for (const session of sessions) {
if (session.expiresAt - OAUTH_REFRESH_SKEW_MS > Date.now()) {
continue;
}
const accessToken = await getValidAccessToken(session.apiUrl);
if (accessToken) {
await pushTokenToTabs(session.apiUrl, accessToken);
}
}
});
// Keep stored sessions fresh so the popup and the injected page token don't expire mid-use. Rotation means each
// refresh mints a new access + refresh token; getValidAccessToken persists them and pushes the access token to tabs.
const ensureRefreshAlarm = async () => {
const existing = await browser.alarms.get(REFRESH_ALARM);
if (!existing) {
await browser.alarms.create(REFRESH_ALARM, { periodInMinutes: 10 });
}
};
ensureRefreshAlarm();
browser.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name !== REFRESH_ALARM) {
return;
}
const sessions = await loadAllSessions();
for (const session of sessions) {
if (session.expiresAt - OAUTH_REFRESH_SKEW_MS > Date.now()) {
continue;
}
const accessToken = await getValidAccessToken(session.apiUrl);
if (accessToken) {
await pushTokenToTabs(session.apiUrl, accessToken);
}
}
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/background/background.ts` around lines 80 - 97, Update the top-level
alarm setup around REFRESH_ALARM to first check whether the alarm already
exists, creating it with the 10-minute period only when absent. Keep the
browser.alarms.onAlarm listener registration at the top level and preserve its
existing refresh handling.

Comment on lines +112 to +117
// Background pushes a rotated access token here on refresh; update it in place so the SDK picks it up without a reload.
messages.listenRuntime('UPDATE_AUTH_TOKEN', async (data) => {
if (sameOrigin(sessionStorage.getItem(API_URL_LOCAL_STORAGE), data.apiUrl)) {
sessionStorage.setItem(AUTH_TOKEN_LOCAL_STORAGE, data.authToken);
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard UPDATE_AUTH_TOKEN against a missing token.

If data.authToken is undefined, sessionStorage.setItem stores the literal string "undefined". The SDK then sends Bearer undefined and in-context editing fails until the page reloads. Reject empty values before the write.

🛡️ Proposed guard
 messages.listenRuntime('UPDATE_AUTH_TOKEN', async (data) => {
-  if (sameOrigin(sessionStorage.getItem(API_URL_LOCAL_STORAGE), data.apiUrl)) {
+  if (
+    data.authToken &&
+    sameOrigin(sessionStorage.getItem(API_URL_LOCAL_STORAGE), data.apiUrl)
+  ) {
     sessionStorage.setItem(AUTH_TOKEN_LOCAL_STORAGE, data.authToken);
   }
 });

Run the following script to confirm the background always sends a token with this message:

#!/bin/bash
rg -n -C6 'UPDATE_AUTH_TOKEN' --glob '*.ts' --glob '*.tsx'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/content/contentScript.ts` around lines 112 - 117, Update the
UPDATE_AUTH_TOKEN listener to validate that data.authToken is present and
non-empty before calling sessionStorage.setItem; retain the same-origin check
and skip the write when the token is missing so an invalid "undefined" value
cannot reach the SDK.

Comment thread src/oauth/oauthClient.ts
Comment on lines +16 to +24
const parseTokenResponse = (
data: Record<string, any>,
previousRefreshToken?: string
): OAuthTokens => ({
accessToken: data.access_token,
// rotation returns a fresh refresh token; if a response omits it, keep the previous one
refreshToken: data.refresh_token ?? previousRefreshToken,
expiresAt: Date.now() + (data.expires_in ?? 0) * 1000,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Validate the token response before building OAuthTokens.

Two gaps exist in parseTokenResponse:

  • If access_token is absent, accessToken becomes undefined. The session is then saved and injected into the page as an invalid token, and the failure surfaces later as an opaque 401.
  • If expires_in is absent, expiresAt equals Date.now(). getValidAccessToken then treats the session as expired on every call and triggers a refresh each time. With refresh-token rotation, this produces a refresh request per token read.

Reject a response without access_token, and use a conservative default lifetime when expires_in is absent.

🛠️ Proposed fix
 const parseTokenResponse = (
   data: Record<string, any>,
   previousRefreshToken?: string
-): OAuthTokens => ({
-  accessToken: data.access_token,
-  // rotation returns a fresh refresh token; if a response omits it, keep the previous one
-  refreshToken: data.refresh_token ?? previousRefreshToken,
-  expiresAt: Date.now() + (data.expires_in ?? 0) * 1000,
-});
+): OAuthTokens => {
+  if (typeof data.access_token !== 'string' || !data.access_token) {
+    throw new Error('Tolgee token endpoint returned no access_token');
+  }
+  const expiresIn =
+    typeof data.expires_in === 'number' && data.expires_in > 0
+      ? data.expires_in
+      : DEFAULT_TOKEN_LIFETIME_SECONDS;
+  return {
+    accessToken: data.access_token,
+    // rotation returns a fresh refresh token; if a response omits it, keep the previous one
+    refreshToken: data.refresh_token ?? previousRefreshToken,
+    expiresAt: Date.now() + expiresIn * 1000,
+  };
+};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/oauth/oauthClient.ts` around lines 16 - 24, Update parseTokenResponse to
validate that data.access_token is present and reject the response before
constructing OAuthTokens when it is missing. Replace the zero-second expires_in
fallback with a conservative default lifetime so getValidAccessToken does not
treat tokens with omitted expiry as immediately expired; preserve the existing
refresh-token fallback behavior.

Comment thread src/oauth/oauthClient.ts
Comment on lines +61 to +73
authorizeUrl.searchParams.set('state', randomUrlSafe());
if (projectId != null) {
authorizeUrl.searchParams.set('project', String(projectId));
}

const redirectResponse = await browser.identity.launchWebAuthFlow({
url: authorizeUrl.toString(),
interactive: true,
});
const code = new URL(redirectResponse).searchParams.get('code');
if (!code) {
throw new Error('Tolgee authorization did not return a code');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Validate the state parameter in the redirect response.

Line 61 generates state and sends it, but the code never compares it with the value returned on the redirect. The control has no effect as written. PKCE and the extension-owned redirect URI limit the practical impact, but OAuth 2.1 requires the client to reject a response whose state does not match the request.

The redirect can also carry error and error_description instead of code. Surface that reason so the popup can show why the login failed.

🛠️ Proposed fix
-  authorizeUrl.searchParams.set('state', randomUrlSafe());
+  const state = randomUrlSafe();
+  authorizeUrl.searchParams.set('state', state);
   if (projectId != null) {
     authorizeUrl.searchParams.set('project', String(projectId));
   }
 
   const redirectResponse = await browser.identity.launchWebAuthFlow({
     url: authorizeUrl.toString(),
     interactive: true,
   });
-  const code = new URL(redirectResponse).searchParams.get('code');
+  const redirectParams = new URL(redirectResponse).searchParams;
+  if (redirectParams.get('state') !== state) {
+    throw new Error('Tolgee authorization returned an unexpected state');
+  }
+  const error = redirectParams.get('error');
+  if (error) {
+    throw new Error(`Tolgee authorization failed: ${error}`);
+  }
+  const code = redirectParams.get('code');
   if (!code) {
     throw new Error('Tolgee authorization did not return a code');
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
authorizeUrl.searchParams.set('state', randomUrlSafe());
if (projectId != null) {
authorizeUrl.searchParams.set('project', String(projectId));
}
const redirectResponse = await browser.identity.launchWebAuthFlow({
url: authorizeUrl.toString(),
interactive: true,
});
const code = new URL(redirectResponse).searchParams.get('code');
if (!code) {
throw new Error('Tolgee authorization did not return a code');
}
const state = randomUrlSafe();
authorizeUrl.searchParams.set('state', state);
if (projectId != null) {
authorizeUrl.searchParams.set('project', String(projectId));
}
const redirectResponse = await browser.identity.launchWebAuthFlow({
url: authorizeUrl.toString(),
interactive: true,
});
const redirectParams = new URL(redirectResponse).searchParams;
if (redirectParams.get('state') !== state) {
throw new Error('Tolgee authorization returned an unexpected state');
}
const error = redirectParams.get('error');
if (error) {
throw new Error(`Tolgee authorization failed: ${error}`);
}
const code = redirectParams.get('code');
if (!code) {
throw new Error('Tolgee authorization did not return a code');
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/oauth/oauthClient.ts` around lines 61 - 73, Update the authorization flow
around authorizeUrl and redirectResponse to retain the generated state, parse
the redirect URL once, and reject responses whose returned state differs or is
missing. Before requiring code, detect redirect error and error_description
parameters and throw an error that includes the provider’s failure reason;
preserve the existing missing-code validation for responses without either a
code or provider error.

Comment thread src/oauth/tokenStore.ts
Comment on lines +35 to +57
export const getValidAccessToken = async (
apiUrl: string
): Promise<string | null> => {
const session = await loadSession(apiUrl);
if (!session) {
return null;
}
if (session.expiresAt - OAUTH_REFRESH_SKEW_MS > Date.now()) {
return session.accessToken;
}
if (!session.refreshToken) {
await clearSession(apiUrl);
return null;
}
try {
const refreshed = await refresh(apiUrl, session.refreshToken);
await saveSession(apiUrl, refreshed);
return refreshed.accessToken;
} catch (e) {
await clearSession(apiUrl);
return null;
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Deduplicate concurrent refreshes for the same apiUrl.

getValidAccessToken reads the session, then refreshes, with no lock. Two overlapping calls for the same apiUrl both read the same refreshToken and both POST to /oauth2/token.

The refresh flow rotates tokens, as stated at Line 22 of src/oauth/oauthClient.ts. The first response invalidates the refresh token. The second request then fails, the catch at Line 53 runs, and clearSession deletes the session that was just refreshed. The user is logged out silently.

This path is reachable. The alarm handler at Line 92 of src/background/background.ts calls getValidAccessToken, and an OAUTH_GET_TOKEN message can arrive in the same service worker at the same time.

Cache the in-flight refresh promise per apiUrl, and clear the session only for authentication failures rather than for every error.

🛠️ Proposed fix
+const inFlight = new Map<string, Promise<string | null>>();
+
 export const getValidAccessToken = async (
   apiUrl: string
 ): Promise<string | null> => {
   const session = await loadSession(apiUrl);
   if (!session) {
     return null;
   }
   if (session.expiresAt - OAUTH_REFRESH_SKEW_MS > Date.now()) {
     return session.accessToken;
   }
   if (!session.refreshToken) {
     await clearSession(apiUrl);
     return null;
   }
-  try {
-    const refreshed = await refresh(apiUrl, session.refreshToken);
-    await saveSession(apiUrl, refreshed);
-    return refreshed.accessToken;
-  } catch (e) {
-    await clearSession(apiUrl);
-    return null;
-  }
+  const key = keyFor(apiUrl);
+  const existing = inFlight.get(key);
+  if (existing) {
+    return existing;
+  }
+  const pending = (async () => {
+    try {
+      const refreshed = await refresh(apiUrl, session.refreshToken!);
+      await saveSession(apiUrl, refreshed);
+      return refreshed.accessToken;
+    } catch (e) {
+      await clearSession(apiUrl);
+      return null;
+    } finally {
+      inFlight.delete(key);
+    }
+  })();
+  inFlight.set(key, pending);
+  return pending;
 };

Note: the service worker can terminate between calls, so this map reduces but does not eliminate the race. Server-side refresh-token replay tolerance, or a single-flight lock in browser.storage, closes the remaining window.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/oauth/tokenStore.ts` around lines 35 - 57, Update getValidAccessToken to
deduplicate concurrent refreshes by caching one in-flight refresh promise per
apiUrl, reusing it for overlapping callers, and removing it when settled.
Preserve existing token/session behavior, but only clear the session when
refresh reports an authentication failure; propagate or retain the session for
other errors instead of treating every failure as invalid credentials.

Comment thread src/popup/reducer.ts
Comment on lines +131 to +155
case 'APPLY_VALUES': {
// sync values with storage/localStorage
apply();
const branchEnabled =
state.credentialsCheck !== null &&
typeof state.credentialsCheck === 'object' &&
'branchingEnabled' in state.credentialsCheck &&
state.credentialsCheck.branchingEnabled;
const effectiveBranch = branchEnabled
? state.values?.branch
: undefined;
return {
...state,
appliedValues: {
apiKey: state.values?.apiKey,
apiUrl: state.values?.apiUrl,
branch: effectiveBranch,
},
storedValues: {
apiKey: state.values?.apiKey,
apiUrl: state.values?.apiUrl,
branch: effectiveBranch,
},
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

APPLY_VALUES drops the OAuth session fields.

APPLY_VALUES rebuilds appliedValues and storedValues from apiKey, apiUrl, and branch only. It discards authToken and projectId. This action is reachable in the OAuth path: handleKeyDown in src/popup/TolgeeDetector.tsx (Line 78-83) dispatches APPLY_VALUES when validateValues(values) passes, and validateValues now accepts authToken without apiKey. The Server field carries onKeyDown={handleKeyDown} and is rendered on the Login tab when the user opens "Change server". Pressing Enter there clears the token from state, pushes empty credentials to the page through SET_CREDENTIALS, and makes storeValues remove the stored OAuth entry.

Preserve the OAuth fields in this action.

🐛 Proposed fix
         const effectiveBranch = branchEnabled
           ? state.values?.branch
           : undefined;
+        const nextValues = {
+          apiKey: state.values?.apiKey,
+          apiUrl: state.values?.apiUrl,
+          branch: effectiveBranch,
+          authToken: state.values?.authToken,
+          projectId: state.values?.projectId,
+        };
         return {
           ...state,
-          appliedValues: {
-            apiKey: state.values?.apiKey,
-            apiUrl: state.values?.apiUrl,
-            branch: effectiveBranch,
-          },
-          storedValues: {
-            apiKey: state.values?.apiKey,
-            apiUrl: state.values?.apiUrl,
-            branch: effectiveBranch,
-          },
+          appliedValues: nextValues,
+          storedValues: nextValues,
         };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case 'APPLY_VALUES': {
// sync values with storage/localStorage
apply();
const branchEnabled =
state.credentialsCheck !== null &&
typeof state.credentialsCheck === 'object' &&
'branchingEnabled' in state.credentialsCheck &&
state.credentialsCheck.branchingEnabled;
const effectiveBranch = branchEnabled
? state.values?.branch
: undefined;
return {
...state,
appliedValues: {
apiKey: state.values?.apiKey,
apiUrl: state.values?.apiUrl,
branch: effectiveBranch,
},
storedValues: {
apiKey: state.values?.apiKey,
apiUrl: state.values?.apiUrl,
branch: effectiveBranch,
},
};
}
case 'APPLY_VALUES': {
// sync values with storage/localStorage
apply();
const branchEnabled =
state.credentialsCheck !== null &&
typeof state.credentialsCheck === 'object' &&
'branchingEnabled' in state.credentialsCheck &&
state.credentialsCheck.branchingEnabled;
const effectiveBranch = branchEnabled
? state.values?.branch
: undefined;
const nextValues = {
apiKey: state.values?.apiKey,
apiUrl: state.values?.apiUrl,
branch: effectiveBranch,
authToken: state.values?.authToken,
projectId: state.values?.projectId,
};
return {
...state,
appliedValues: nextValues,
storedValues: nextValues,
};
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/popup/reducer.ts` around lines 131 - 155, Update the APPLY_VALUES case in
the reducer so appliedValues and storedValues preserve the existing OAuth
authToken and projectId alongside apiKey, apiUrl, and the conditionally
effective branch. Ensure the OAuth values remain intact when APPLY_VALUES is
triggered through the Server field without changing the existing branchEnabled
behavior.

Comment on lines +85 to +118
const handleConnect = async () => {
const apiUrl = values?.apiUrl || DEFAULT_SERVER;
setConnecting(true);
try {
// Hint the project the page is configured for (exposed via the handshake), so the consent screen pre-selects it
// and the minted token is scoped to it. On a public project the hint resolves via the community floor.
const hinted = (libConfig?.config as { projectId?: number | string })
?.projectId;
const projectId =
hinted !== undefined && hinted !== '' ? Number(hinted) : undefined;
// Capture the target tab now: launchWebAuthFlow closes the popup, so the background does the injection and needs
// the tab id up front.
const [activeTab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
const res = (await sendToBackground('OAUTH_LOGIN', {
apiUrl,
projectId,
tabId: activeTab?.id,
})) as {
accessToken?: string;
error?: string;
};
if (res?.accessToken) {
dispatch({
type: 'OAUTH_APPLY',
payload: { apiUrl, authToken: res.accessToken },
});
}
} finally {
setConnecting(false);
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Surface the login error to the user.

The response type declares error, but the handler ignores it. If the OAuth flow fails, for example when the user closes the consent window or the token exchange fails, the button returns to its idle label and the popup shows no reason. Store the error and render it in the Login tab.

🐛 Proposed change
+  const [connectError, setConnectError] = useState<string | null>(null);
...
   const handleConnect = async () => {
     const apiUrl = values?.apiUrl || DEFAULT_SERVER;
     setConnecting(true);
+    setConnectError(null);
     try {
...
       if (res?.accessToken) {
         dispatch({
           type: 'OAUTH_APPLY',
           payload: { apiUrl, authToken: res.accessToken },
         });
+      } else {
+        setConnectError(res?.error || 'Connection failed');
       }
     } finally {
       setConnecting(false);
     }
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/popup/TolgeeDetector.tsx` around lines 85 - 118, Update handleConnect to
capture and store res.error when OAuth login fails, including cases where no
accessToken is returned, and preserve clearing the error on a new attempt or
successful login. Render the stored error message in the Login tab near the
connect control so the user sees why authentication failed.

Comment on lines +359 to +374
{serverOpen ? (
serverField
) : (
<Typography variant="body2">
Connect to your account on{' '}
<Link
href={values?.apiUrl || DEFAULT_SERVER}
target="_blank"
rel="noreferrer"
underline="hover"
>
{serverHost}
</Link>{' '}
and start translating.
</Typography>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Validate the scheme before you use the server value as a link target.

values?.apiUrl comes from the editable Server field and goes straight into href. A value such as javascript:... produces an executable link inside the extension popup, which runs with extension privileges. Restrict the href to http: and https:, and fall back to DEFAULT_SERVER.

🛡️ Proposed guard
+    let serverLink = DEFAULT_SERVER;
+    try {
+      const parsed = new URL(values?.apiUrl || DEFAULT_SERVER);
+      if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
+        serverLink = parsed.toString();
+      }
+    } catch {
+      // keep the default when the value is not a full URL yet
+    }
...
                   <Link
-                    href={values?.apiUrl || DEFAULT_SERVER}
+                    href={serverLink}
                     target="_blank"
                     rel="noreferrer"
                     underline="hover"
                   >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{serverOpen ? (
serverField
) : (
<Typography variant="body2">
Connect to your account on{' '}
<Link
href={values?.apiUrl || DEFAULT_SERVER}
target="_blank"
rel="noreferrer"
underline="hover"
>
{serverHost}
</Link>{' '}
and start translating.
</Typography>
)}
{serverOpen ? (
serverField
) : (
<Typography variant="body2">
Connect to your account on{' '}
<Link
href={serverLink}
target="_blank"
rel="noreferrer"
underline="hover"
>
{serverHost}
</Link>{' '}
and start translating.
</Typography>
)}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/popup/TolgeeDetector.tsx` around lines 359 - 374, Validate values?.apiUrl
before passing it to the Link href in the server connection UI: accept only
http: and https: URLs, and use DEFAULT_SERVER for any other value, including
malformed or javascript: schemes. Keep the existing serverHost display and link
behavior unchanged for valid URLs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant